package com.github.rxyor.common.util.okhttpclient;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.rxyor.common.core.model.R;
import com.github.rxyor.common.util.okhttpclient.OkHttpClients.Body.BodyType;
import java.io.File;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;

/**
 *<p>
 * OkHttpClient 链式化
 *</p>
 *
 * @author liuyang
 * @since 1.0.0
 * @version 2020-06-11 08:53:56
 */
public class OkHttpClients {

    private final static int CONNECT_TIMEOUT_TIME = 3;
    private final static int WRITE_TIMEOUT_TIME = 3;
    private final static int READ_TIMEOUT_TIME = 10;

    /**
     * request headers
     */
    private final Map<String, String> headers = new HashMap<>(16);
    /**
     * request params
     */
    private final Map<String, String> params = new HashMap<>(16);
    /**
     * request url
     */
    private String url;
    /**
     * JSON响应体类型
     */
    private TypeReference<?> dataType;
    /**
     * request body config
     */
    private Body body;

    /**
     * request config
     */
    private Config config;

    /**
     * http method
     */
    private Method method;

    /**
     *callback
     */
    private Callback callBack;

    private OkHttpClients() {
    }

    private OkHttpClients(String url) {
        this.url = url;
    }

    public static OkHttpClients get(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.GET;
        return clients;
    }

    public static OkHttpClients post(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.POST;
        return clients;
    }

    public static OkHttpClients put(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.PUT;
        return clients;
    }

    public static OkHttpClients delete(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.DELETE;
        return clients;
    }

    public static OkHttpClients head(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.HEAD;
        return clients;
    }

    public static OkHttpClients patch(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.PATCH;
        return clients;
    }


    public static OkHttpClients options(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.OPTIONS;
        return clients;
    }

    public static OkHttpClients trace(String url) {
        OkHttpClients clients = new OkHttpClients(url);
        clients.method = Method.TRACE;
        return clients;
    }

    private static OkHttpClients url(String url) {
        if (StringUtils.isBlank(url)) {
            throw new IllegalArgumentException("url can't be blank");
        }
        return new OkHttpClients(url);
    }

    /**
     *<p>
     * set url params
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:26:01
     * @param key key
     * @param value value
     * @return []
     */
    public OkHttpClients param(String key, String value) {
        if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
            this.params.put(key.trim(), value);
        }
        return this;
    }

    public OkHttpClients param(Map<String, Object> params) {
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, Object> e : params.entrySet()) {
                if (e != null) {
                    this.param(e.getKey(), toStringValue(e.getValue()));
                }
            }
        }
        return this;
    }

    /**
     *<p>
     * set headers
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:26:16
     * @param key key
     * @param value value
     * @return []
     */
    @SuppressWarnings("all")
    public OkHttpClients header(String key, String value) {
        if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
            this.headers.put(key.trim(), value);
        }
        return this;
    }

    public OkHttpClients header(Map<String, Object> headers) {
        if (headers != null && !headers.isEmpty()) {
            for (Map.Entry<String, Object> e : headers.entrySet()) {
                if (e != null) {
                    this.header(e.getKey(), toStringValue(e.getValue()));
                }
            }
        }
        return this;
    }

    /**
     *<p>
     * response string to Java Type config
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:26:43
     * @param dataType dataType
     * @return []
     */
    public OkHttpClients dataType(TypeReference<?> dataType) {
        this.dataType = dataType;
        return this;
    }

    /**
     *<p>
     *  async callback
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:27:39
     * @param callback callback
     * @return []
     */
    public OkHttpClients callback(Callback callback) {
        Objects.requireNonNull(callback, "callback can't be null");
        this.callBack = callback;
        return this;
    }

    public Body body() {
        this.body = new Body(this);
        return this.body;
    }

    /**
     *<p>
     *  OkHttpClient config
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:28:15
     * @return []
     */
    public Config config() {
        this.config = new Config(this);
        return this.config;
    }

    /**
     *<p>
     * async send request
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:29:06
     * @return []
     */
    public Call asyncExecute() {
        Call call = doExecute();
        if (callBack != null) {
            call.enqueue(callBack);
        }
        return call;
    }

    /**
     *<p>
     *  sync send request
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:29:36
     * @return []
     */
    @SuppressWarnings("all")
    public R execute() {
        try {
            Response response = doExecute().execute();
            if (!response.isSuccessful()) {
                return R.fail(response.code(), response.message());
            }

            try {
                ResponseBody body = response.body();
                if (body == null) {
                    return null;
                }

                String json = body.string();
                if (dataType == null
                    || String.class.getName().equals(dataType.getType().getTypeName())
                    || CharSequence.class.getName().equals(dataType.getType().getTypeName())) {
                    return R.success(json);
                }
                return R.success(JSON.parseObject(json, dataType));
            } catch (IOException e) {
                throw new RuntimeException(e);
            } catch (Exception e) {
                throw new RuntimeException("Json转换异常, 错误:\n", e);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Call doExecute() {
        Request request = buildRequest();
        OkHttpClient client = this.buildClient();
        return client.newCall(request);
    }

    private Request buildRequest() {
        Request.Builder builder = new Request.Builder();
        builder.url(this.buildHttpUrl());
        this.headers.forEach(builder::addHeader);
        RequestBody requestBody = this.buildRequestBody();
        if (method != Method.GET && requestBody != null) {
            builder.method(method.name(), requestBody);
        }
        return builder.build();
    }

    private OkHttpClient buildClient() {
        if (this.config == null) {
            this.config();
        }

        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(this.config.connectTimeout, TimeUnit.SECONDS);
        builder.writeTimeout(this.config.writeTimeout, TimeUnit.SECONDS);
        builder.readTimeout(this.config.readTimeout, TimeUnit.SECONDS);
        builder.retryOnConnectionFailure(true);

        //skip ssl verify
        if (this.config.disableSLL) {
            builder.sslSocketFactory(SSLUtil.sslSocketFactory(), SSLUtil.x509TrustManager());
            builder.hostnameVerifier((s, sslSession) -> true);
        }
        return builder.build();
    }

    private HttpUrl buildHttpUrl() {
        HttpUrl httpUrl = HttpUrl.parse(this.url);
        if (httpUrl == null) {
            throw new IllegalArgumentException(String.format("url:[%s] is illegal", url));
        }
        HttpUrl.Builder builder = httpUrl.newBuilder();
        this.params.forEach(builder::addQueryParameter);
        return builder.build();
    }

    private RequestBody buildRequestBody() {
        if (this.body == null || this.body.bodyType == BodyType.NULL || this.body.body == null) {
            return null;
        }

        if (this.body.body instanceof String) {
            return RequestBody.create(body.mediaType, (String) this.body.body);
        } else if (this.body.body instanceof File) {
            return RequestBody.create(body.mediaType, (File) this.body.body);
        } else if (this.body.body instanceof byte[]) {
            return RequestBody.create(body.mediaType, (byte[]) this.body.body);
        }
        return null;
    }

    /**
     * 将支持的参数类型转换为String
     *
     * @param value 参数值
     * @return String
     */
    private String toStringValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof String) {
            return (String) value;
        }
        if (value instanceof CharSequence) {
            return value.toString();
        }
        if (value instanceof Boolean) {
            return value.toString();
        }
        if (value instanceof Byte) {
            return value.toString();
        }
        if (value instanceof Integer) {
            return value.toString();
        }
        if (value instanceof Long) {
            return value.toString();
        }
        if (value instanceof Float) {
            return value.toString();
        }
        if (value instanceof Double) {
            return value.toString();
        }
        throw new IllegalArgumentException(value + "is not supported param type");
    }

    private enum Method {
        GET,
        POST,
        PUT,
        DELETE,
        HEAD,
        PATCH,
        TRACE,
        OPTIONS
    }

    /**
     *<p>
     *  ContentType Const Class
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:37:30
     * @since 1.0.0
     */
    public interface MediaTypeConst {

        MediaType APPLICATION_JSON = MediaType.parse("application/json");
    }

    /**
     *<p>
     *  custom request body config
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:37:43
     * @since 1.0.0
     */
    public static class Body {

        private final OkHttpClients okHttpClients;
        private MediaType mediaType;
        private BodyType bodyType;
        private Object body;

        private Body(OkHttpClients okHttpClients) {
            Objects.requireNonNull(okHttpClients, "okHttpClients can't be null");
            this.okHttpClients = okHttpClients;
        }

        public Body contentType(MediaType contentType) {
            this.mediaType = contentType;
            return this;
        }

        public OkHttpClients json(Object json) {
            if (json == null) {
                this.bodyType = BodyType.NULL;
                return this.okHttpClients;
            }

            this.bodyType = BodyType.STRING;
            this.mediaType = MediaTypeConst.APPLICATION_JSON;

            if (json instanceof CharSequence) {
                boolean isValid = JSON.isValid(json.toString());
                if (!isValid) {
                    throw new IllegalArgumentException("body format is error:" + json.toString());
                }
            } else if (json instanceof JSONObject) {
                this.body = ((JSONObject) json).toJSONString();
            } else if (json instanceof JSONArray) {
                this.body = ((JSONArray) json).toJSONString();
            } else {
                this.body = JSON.toJSONString(json);
            }
            return this.okHttpClients;
        }

        public OkHttpClients string(String content) {
            if (content == null) {
                this.bodyType = BodyType.NULL;
                return this.okHttpClients;
            }

            this.bodyType = BodyType.STRING;
            this.body = content;
            return this.okHttpClients;

        }

        public OkHttpClients bytes(byte[] bytes) {
            if (bytes == null) {
                this.bodyType = BodyType.NULL;
                return this.okHttpClients;
            }

            this.bodyType = BodyType.BYTES;
            this.body = bytes;
            return this.okHttpClients;

        }

        public OkHttpClients file(File file) {
            if (file == null) {
                this.bodyType = BodyType.NULL;
                return this.okHttpClients;
            }

            this.bodyType = BodyType.FILE;
            this.body = file;
            return this.okHttpClients;
        }

        public enum BodyType {
            NULL,
            STRING,
            BYTES,
            FILE
        }
    }

    /**
     *<p>
     *  custom request config
     *</p>
     *
     * @author liuyang
     * @date 2020-06-11 周四 13:38:15
     * @since 1.0.0
     */
    public static class Config {

        private final OkHttpClients okHttpClients;

        private int connectTimeout = CONNECT_TIMEOUT_TIME;
        private int writeTimeout = WRITE_TIMEOUT_TIME;
        private int readTimeout = READ_TIMEOUT_TIME;
        private boolean disableSLL = false;

        private Config(OkHttpClients okHttpClients) {
            Objects.requireNonNull(okHttpClients, "okHttpClients can't be null");
            this.okHttpClients = okHttpClients;
        }

        public Config connectTimeout(int connectTimeout) {
            if (connectTimeout < 0) {
                throw new IllegalArgumentException("connectTimeout must >= 0");
            }
            this.connectTimeout = connectTimeout;
            return this;
        }

        public Config writeTimeout(int writeTimeout) {
            if (writeTimeout < 0) {
                throw new IllegalArgumentException("writeTimeout must >= 0");
            }
            this.writeTimeout = writeTimeout;
            return this;
        }

        public Config readTimeout(int readTimeout) {
            if (readTimeout < 0) {
                throw new IllegalArgumentException("readTimeout must >= 0");
            }
            this.readTimeout = readTimeout;
            return this;
        }

        public OkHttpClients disableSSL() {
            this.disableSLL = true;
            return this.okHttpClients;
        }
    }

    private static class SSLUtil {

        private static SSLSocketFactory sslSocketFactory() {
            try {
                final TrustManager[] trustAllCerts = new TrustManager[]{
                    x509TrustManager()
                };

                final SSLContext sslContext = SSLContext.getInstance("SSL");
                sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

                return sslContext.getSocketFactory();
            } catch (Exception e) {
                throw new RuntimeException("SSLContext构造失败," + e.getMessage(), e);
            }
        }

        private static X509TrustManager x509TrustManager() {
            return new X509TrustManager() {
                @Override
                public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
                    String authType) {
                }

                @Override
                public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
                    String authType) {
                }

                @Override
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            };
        }
    }

}
